package org.datacoder.geocoding.jurisdiction

import org.apache.commons.collections.BidiMap
import org.apache.commons.collections.bidimap.DualHashBidiMap
import org.datacoder.AllService
import org.datacoder.SampleData
import org.datacoder.geocoding.GeocodingUtils
import org.datacoder.util.DatacoderConstants
import org.datacoder.util.GeneralUtils
import org.datacoder.util.Pair
import com.vividsolutions.jts.geom.*
import com.vividsolutions.jts.geom.impl.*
import com.vividsolutions.jts.operation.valid.*

/**
 * A tool for reading Census geographic data.
 */
class JurisdictionLocator {

    private static final int WIPE_INTERVAL = 500
    private static final String whitespaceRegex = "\\s+"
    private static final GeometryFactory geometryFactory
    private static BidiMap stateIdFipsCodeMap
    private static BidiMap countyIdFipsCodeMap
    private static BidiMap stateNamePostalCodeMap

    private static final String CD_PATH = DatacoderConstants.DB_GEO_PATH + "/congress"
    private static final String COUNTY_PATH = DatacoderConstants.DB_GEO_PATH + "/counties"
    private static final String STATE_PATH = DatacoderConstants.DB_GEO_PATH + "/states"
    private static final String MUNIC_PATH = DatacoderConstants.DB_GEO_PATH + "/municipalities"
    private static final String SH_PATH = DatacoderConstants.DB_GEO_PATH + "/state_house"
    private static final String SS_PATH = DatacoderConstants.DB_GEO_PATH + "/state_senate"
    private static final String ZIP_PATH = DatacoderConstants.DB_GEO_PATH + "/zip_codes"
    private static final String VD_PATH = DatacoderConstants.DB_GEO_PATH + "/voting_districts"

    private static final String CD_LOCFILE = CD_PATH + "/cd_location.dat"
    private static final String CD_METAFILE = CD_PATH + "/cd_metadata.dat"
    private static final String COUNTY_LOCFILE = COUNTY_PATH + "/county_location.dat"
    private static final String COUNTY_METAFILE = COUNTY_PATH + "/county_metadata.dat"
    private static final String STATE_LOCFILE = STATE_PATH + "/state_location.dat"
    private static final String STATE_METAFILE = STATE_PATH + "/state_metadata.dat"
    private static final String STATE_POSTALCODEFILE = STATE_PATH + "/state_postalcode.txt"
    private static final String MUNIC_PREFIX = "pl"
    private static final String MUNIC_LOCSUFFIX = "_d00.dat"
    private static final String MUNIC_METASUFFIX = "_d00a.dat"
    private static final String SH_PREFIX = "sl"
    private static final String SH_LOCSUFFIX = "_d11.dat"
    private static final String SH_METASUFFIX = "_d11a.dat"
    private static final String SS_PREFIX = "su"
    private static final String SS_LOCSUFFIX = "_d11.dat"
    private static final String SS_METASUFFIX = "_d11a.dat"
    private static final String ZIP_PREFIX = "zt"
    private static final String ZIP_LOCSUFFIX = "_d00.dat"
    private static final String ZIP_METASUFFIX = "_d00a.dat"
    private static final String VD_PREFIX = "vt"
    private static final String VD_LOCSUFFIX = "_d00.dat"
    private static final String VD_METASUFFIX = "_d00a.dat"
    
    def jurisdictionService
    def countryService
    def stateService
    def countyService
    def municipalityService
    def congressionalDistrictService
    def senatorialDistrictService
    def representativeDistrictService
    def zipCodeService
    def votingDistrictService
    def sessionFactory

    public JurisdictionLocator(AllService allService){
        this.geometryFactory = GeocodingUtils.geometryFactory
        this.jurisdictionService = allService.jurisdictionService
        this.countryService = allService.countryService
        this.stateService = allService.stateService
        this.countyService = allService.countyService
        this.municipalityService = allService.municipalityService
        this.congressionalDistrictService = allService.congressionalDistrictService
        this.senatorialDistrictService = allService.senatorialDistrictService
        this.representativeDistrictService = allService.representativeDistrictService
        this.zipCodeService = allService.zipCodeService
        this.votingDistrictService = allService.votingDistrictService
        this.sessionFactory = allService.sessionFactory
    }
    /**
     *  Create all jurisdictions (including zip codes).  Each jurisdiction type
     *  is handled separately, but they all follow the same basic format.
     *
     *  1. Get or construct the name of the files containing the needed data.
     *     There are two--one contains metadata (the "metafile") on the
     *     jurisdictions in question, while the other contains the actual
     *     latitude/longitude data (the "locfile").
     *  2. Use readCensusData() on the two files, with a few additional
     *     parameters defining the structure of the metadata and determining
     *     how to handle disjoint jurisdictions (the islands of Hawaii, for
     *     example).  This method returns a list of pairs linking metadata to
     *     polygons defining jurisdictions.
     *  3. Use censusDataToCentroids() on this list to convert it to a list of
     *     pairs linking metadata to the centroids of the jurisdictions.
     *  4. Iterate through each pair, using the metadata (pair.first) and the
     *     latitude and longitude (pair.second) to construct the relevant
     *     Jurisdiction object.
     *
     *  Some jurisdictions, such as municipalities, are processed
     *  state-by-state, so there is an extra layer around this basic process to
     *  handle this iteration.
     *
     *  There are many rough spots in the data.  For example, Nebraska is
     *  unicameral, a number of states have legislatures with
     *  not-strictly-numeric naming systems, and there is even a typo in the
     *  state senatorial data for Nevada.  These instances are handled with
     *  case-by-case conditionals.  Moreover, the nomenclature used by the
     *  Census is not identical to that used by VoteSmart, which makes matching
     *  their data difficult.  This method renames data from the Census to match
     *  the format of VoteSmart data as best it can, again with case-by-case
     *  conditionals.
     */
    public void createJurisdictionData(List<String> stateIds){
        Country us
        List<String> stateNames = new ArrayList<String>()

        // Determine states to load
        stateNamePostalCodeMap = mapStateNamesToPostalCodes()
        for(stateId in stateIds){
            stateNames.add(stateNamePostalCodeMap.getKey(stateId))
        }
        
        // Country
        print "Loading countries..."
        us = createCountryData()
        println "done."

        // States
        print "Loading states..."
        createStateData(stateNames, us)
        println "done."

        // Counties
        print "Loading counties..."
        createCountyData()
        println "done."

        // Municipalities
        print "Loading municipalities..."
        createMunicipalityData(stateNames)
        if(stateNames.size >= 2){
            println ""
            println "Municipalities done."
        }else{
            println "done."
        }

        // Congressional Districts
        print "Loading congressional districts..."
        createCongressionalData()
        println "done."

        // Senatorial Districts
        print "Loading senatorial districts..."
        createSenatorialData(stateNames)
        if(stateNames.size >= 2){
            println ""
            println "Senatorial districts done."
        }else{
            println "done."
        }

        // Representative Districts
        print "Loading representative districts..."
        createRepresentativeData(stateNames)
        if(stateNames.size >= 2){
            println ""
            println "Representative districts done."
        }else{
            println "done."
        }

        // Zip Codes
        print "Loading zip codes..."
        createZipCodeData(stateNames)
        if(stateNames.size >= 2){
            println ""
            println "Zip codes done."
        }else{
            println "done."
        }

        // Voting Districts
        print "Loading voting districts..."
        createVotingDistrictData(stateNames)
        if(stateNames.size >= 2){
            println ""
            println "Voting districts done."
        }else{
            println "done."
        }

        // Flush everything
        GeneralUtils.wipeReferences(sessionFactory)
    }
    private Country createCountryData(){
        Country us = countryService.getByNameOrCreate("US", "United States")
        countryService.save(us)
        return us
    }
    private void createStateData(List<String> stateNames, Country us){
        List<Pair<CensusMetadata, MultiPolygon>> censusData = readCensusData(STATE_METAFILE, STATE_LOCFILE, [1], true, [0])
        List<MultiPolygon> stateMultiPolygons = new LinkedList<MultiPolygon>()
        int totalPolygons = 0
        for(pair in censusData){
            String name = pair.first.name
            if(!listContainsString(stateNames, name.toUpperCase())){
                continue
            }
            MultiPolygon polygon = pair.second
            String postalCode = stateNamePostalCodeMap.get(name.toUpperCase())
            State state = stateService.getByNameAndCountryOrCreate(postalCode, us, name)
            state.multiPolygon = polygon
            stateMultiPolygons.add(polygon)
            totalPolygons += polygon.getNumGeometries()
            stateService.save(state)
        }
        Polygon[] statePolygons = new Polygon[totalPolygons]
        int j = 0
        for(stateMultiPolygon in stateMultiPolygons){
            for(int k = 0; k < stateMultiPolygon.getNumGeometries(); k++){
                statePolygons[j++] = (Polygon)stateMultiPolygon.getGeometryN(k)
            }
        }
        us.multiPolygon = geometryFactory.createMultiPolygon(statePolygons)
        countryService.save(us)
        stateIdFipsCodeMap = mapStateIdsToFipsCodes(STATE_METAFILE)
        GeneralUtils.wipeReferences(sessionFactory)
    }
    public void createCountyData(){
        mapStateIdsToFipsCodes(STATE_METAFILE)
        List<Pair<CensusMetadata, MultiPolygon>> censusData = readCensusData(COUNTY_METAFILE, COUNTY_LOCFILE, [1,2], true, [0, 1])
        for(pair in censusData){
            if(pair.first.entityCode.equals("13") || pair.first.entityCode.equals("14")){
                continue
            }
            State state = State.get(stateIdFipsCodeMap.getKey(pair.first.fipsCodes.get(0)))
            if(state == null){
                continue
            }
            String name = pair.first.name
            String longName = "${pair.first.name} ${pair.first.entityName}"
            MultiPolygon polygon = pair.second
            County county = countyService.getByNameAndStateOrCreate(name, state, longName)
            county.multiPolygon = polygon
            countyService.save(county)
        }
        countyIdFipsCodeMap = mapCountyIdsToFipsCodes(COUNTY_METAFILE)
        GeneralUtils.wipeReferences(sessionFactory)
    }
    private void createMunicipalityData(List<String> stateNames){
        int i
        if(stateNames.size >= 2){
            i = 0
            println ""
        }
        for(theStateId in stateIdFipsCodeMap.keySet()){
            if(stateNames.size >= 2){
                if(i > 0 && i % 10 == 0){
                    println ""
                }
                i++
                print "."
            }
            State state = State.get(theStateId)
            if(state == null){
                continue
            }
            String fipsCode = stateIdFipsCodeMap.get(theStateId)
            String metafile = "${MUNIC_PATH}/${MUNIC_PREFIX}${fipsCode}${MUNIC_METASUFFIX}"
            String locfile = "${MUNIC_PATH}/${MUNIC_PREFIX}${fipsCode}${MUNIC_LOCSUFFIX}"
            List<Pair<CensusMetadata, MultiPolygon>> censusData = readCensusData(metafile, locfile, [1,2], true, [0, 1])
            int l = 0
            for(pair in censusData){
                if(l > 0 && l % WIPE_INTERVAL == 0){
                    GeneralUtils.wipeReferences(sessionFactory)
                }
                l++
                String name = pair.first.name
                String longName = name
                MultiPolygon polygon = pair.second
                Municipality munic = municipalityService.getByNameAndStateOrCreate(name, state, longName)
                munic.multiPolygon = polygon
                municipalityService.save(munic)
            }
        }
    }
    private void createCongressionalData(){
        List<Pair<CensusMetadata, MultiPolygon>> censusData = readCensusData(CD_METAFILE, CD_LOCFILE, [1,2], true, [0, 1])
        for(pair in censusData){
            if(pair.first.entityCode.equals("C3") || pair.first.entityCode.equals("C4")){
                continue
            }
            State state = State.get(stateIdFipsCodeMap.getKey(pair.first.fipsCodes.get(0)))
            if(state == null){
                continue
            }
            String name = pair.first.name
            if(name.equals("One")){
                name = "At-Large"
            }
            String longName = "${state.name} ${pair.first.entityName} ${pair.first.name}"
            MultiPolygon polygon = pair.second
            CongressionalDistrict cd = congressionalDistrictService.getByNameAndStateOrCreate(name, state, longName)
            cd.multiPolygon = polygon
            congressionalDistrictService.save(cd)
        }
        GeneralUtils.wipeReferences(sessionFactory)
    }
    private void createSenatorialData(List<String> stateNames){
        int i
        if(stateNames.size >= 2){
            i = 0
            println ""
        }
        for(theStateId in stateIdFipsCodeMap.keySet()){
            if(stateNames.size >= 2){
                if(i > 0 && i % 10 == 0){
                    println ""
                }
                i++
                print "."
            }
            State state = State.get(theStateId)
            if(state == null){
                continue
            }
            String fipsCode = stateIdFipsCodeMap.get(theStateId)
            String metafile = "${SS_PATH}/${SS_PREFIX}${fipsCode}${SS_METASUFFIX}"
            String locfile = "${SS_PATH}/${SS_PREFIX}${fipsCode}${SS_LOCSUFFIX}"
            List<Pair<CensusMetadata, MultiPolygon>> censusData = readCensusData(metafile, locfile, [1,2,6], true, [0, 1])
            int l = 0
            for(pair in censusData){
                if(l > 0 && l % WIPE_INTERVAL == 0){
                    GeneralUtils.wipeReferences(sessionFactory)
                }
                l++
                String name = pair.first.name
                if(name == "State Senate Districts not defined"){
                    continue
                }
                if(name.charAt(0) == '0'){
                    String[] nameTokens = name.split('0+')
                    name = name.substring(name.indexOf(nameTokens[1]))
                }else if(state.name.equals("MA")){
                    name = name.replace(" & ", " and ")
                    if(name.endsWith(" District")){
                        name = name.substring(0, name.length() - " District".length())
                    }
                }
                if(state.name.equals("NV")){
                    name = name.replace(" County Senatorial District ", " ")
                    name = name.replace(" County Senatorial Distict ", " ")
                    if(name.equals("Rural Nevada")){
                        name = "Northern Nevada"
                    }
                }
                String longName = name
                MultiPolygon polygon = pair.second
                SenatorialDistrict sd = senatorialDistrictService.getByNameAndStateOrCreate(name, state, longName)
                sd.multiPolygon = polygon
                senatorialDistrictService.save(sd)
            }
        }

    }
    private void createRepresentativeData(List<String> stateNames){
        int i
        if(stateNames.size >= 2){
            i = 0
            println ""
        }
        for(theStateId in stateIdFipsCodeMap.keySet()){
            if(stateNames.size >= 2){
                if(i > 0 && i % 10 == 0){
                    println ""
                }
                i++
                print "."
            }
            State state = State.get(theStateId)
            if(state == null){
                continue
            }
            if(state.name.equals("NE")){
                continue
            }
            String fipsCode = stateIdFipsCodeMap.get(theStateId)
            String metafile = "${SH_PATH}/${SH_PREFIX}${fipsCode}${SH_METASUFFIX}"
            String locfile = "${SH_PATH}/${SH_PREFIX}${fipsCode}${SH_LOCSUFFIX}"
            List<Pair<CensusMetadata, MultiPolygon>> censusData = readCensusData(metafile, locfile, [1,2,6], true, [0, 1])
            int l = 0
            for(pair in censusData){
                if(l > 0 && l % WIPE_INTERVAL == 0){
                    GeneralUtils.wipeReferences(sessionFactory)
                }
                l++
                String name = pair.first.name
                if(name == "State House Districts not defined"){
                    continue
                }
                if(name.charAt(0) == '0'){
                    String[] nameTokens = name.split('0+')
                    name = name.substring(name.indexOf(nameTokens[1]))
                }else if(state.name.equals("NH")){
                    String num = name.substring(0, name.indexOf(','))
                    String county = name.split(" ")[1]
                    name = "${county} ${num}"
                }else if(state.name.equals("MA")){
                    name = name.replace(" & ", " and ")
                    if(name.endsWith(" District")){
                        name = name.substring(0, name.length() - " District".length())
                    }
                }else if(state.name.equals("AK")){
                    name = name.substring(0, name.indexOf(','))
                }else if(state.name.equals("VT")){
                    if(!Character.isDigit(name.charAt(name.length() - 1))){
                        name = name + "-1"
                    }
                }
                String longName = name
                MultiPolygon polygon = pair.second
                RepresentativeDistrict rd = representativeDistrictService.getByNameAndStateOrCreate(name, state, longName)
                rd.multiPolygon = polygon
                representativeDistrictService.save(rd)
            }
        }
    }
    private void createZipCodeData(List<String> stateNames){
        int i
        if(stateNames.size >= 2){
            i = 0
            println ""
        }
        for(theStateId in stateIdFipsCodeMap.keySet()){
            if(stateNames.size >= 2){
                if(i > 0 && i % 10 == 0){
                    println ""
                }
                i++
                print "."
            }
            State state = State.get(theStateId)
            if(state == null){
                continue
            }
            String fipsCode = stateIdFipsCodeMap.get(theStateId)
            String metafile = "${ZIP_PATH}/${ZIP_PREFIX}${fipsCode}${ZIP_METASUFFIX}"
            String locfile = "${ZIP_PATH}/${ZIP_PREFIX}${fipsCode}${ZIP_LOCSUFFIX}"
            List<Pair<CensusMetadata, MultiPolygon>> censusData = readCensusData(metafile, locfile, [1], true, [0])
            int l = 0
            for(pair in censusData){
                if(l > 0 && l % WIPE_INTERVAL == 0){
                    GeneralUtils.wipeReferences(sessionFactory)
                }
                l++
                String name = pair.first.name
                String longName = state.name + " " + name
                MultiPolygon polygon = pair.second
                ZipCode zc = zipCodeService.getByNameAndStateOrCreate(name, state, longName)
                zc.multiPolygon = polygon
                zipCodeService.save(zc)
            }
        }
    }
    private void createVotingDistrictData(List<String> stateNames){
        int i
        if(stateNames.size >= 2){
            i = 0
            println ""
        }
        for(theStateId in stateIdFipsCodeMap.keySet()){
            if(stateNames.size >= 2){
                if(i > 0 && i % 10 == 0){
                    println ""
                }
                i++
                print "."
            }
            State state = State.get(theStateId)
            if(state == null){
                continue
            }
            String fipsCode = stateIdFipsCodeMap.get(theStateId)
            String metafile = "${VD_PATH}/${VD_PREFIX}${fipsCode}${VD_METASUFFIX}"
            String locfile = "${VD_PATH}/${VD_PREFIX}${fipsCode}${VD_LOCSUFFIX}"
            List<Pair<CensusMetadata, MultiPolygon>> censusData
            try{
                censusData = readCensusData(metafile, locfile, [1,2,3], true, [1,2])
            }catch(FileNotFoundException fnfe){
                continue
            }
            int l = 0
            for(pair in censusData){
                if(l > 0 && l % WIPE_INTERVAL == 0){
                    GeneralUtils.wipeReferences(sessionFactory)
                }
                l++
                String name = pair.first.name
                if(name == "Voting Districts not defined"){
                    continue
                }
                String longName = state.name + " " + name
                MultiPolygon polygon = pair.second
                County county = County.get(countyIdFipsCodeMap.getKey(pair.first.fipsCodes.get(0) + pair.first.fipsCodes.get(1)))
                if(county == null){
                    println ""
                    println "ERROR: No county for key ${pair.first.fipsCodes.get(0) + pair.first.fipsCodes.get(1)}, VD Long Name: ${longName}"
                    continue
                }
                VotingDistrict vd = votingDistrictService.getByNameAndCountyOrCreate(name, county, longName)
                vd.multiPolygon = polygon
                votingDistrictService.save(vd)
            }
        }
    }

    /* READ DATA */
    /**
     *  Read a metadata file and a location file, and return a list of pairs
     *  linking each chunk of metadata to the polygon defining its
     *  jurisdictions.  This method uses several approaches to get around bad
     *  data; it generally skips data if the data cannot be processed, instead
     *  of crashing.
     *
     *  fipsCodeFields is a list of integers declaring which fields of a
     *  metadata chunk are FIPS codes.  (FIPS codes are government-assigned IDs
     *  which serve as a rough sort of primary key for the census data.)
     *
     *  mergePolygons determines whether this method will attempt to merge
     *  the polygons of a disjoint jurisdiction (such as the islands of Hawaii)
     *  into a single MultiPolygon.
     *
     *  mergeFipsCodeFields is only relevant if mergePolygons is true.  It
     *  declares which FIPS codes should be used to identify polygons that
     *  should be merged.  The numbers in this list correspond to which FIPS
     *  code to use, not which field in the metadata chunk.
     */
    private List<Pair<CensusMetadata, MultiPolygon>> readCensusData(String metadataFile, String locationFile, List<Integer> fipsCodeFields, boolean mergePolygons, List<Integer> mergeFipsCodeFields){
        List<Pair<CensusMetadata, MultiPolygon>> censusMetadataMultiPolygons = new ArrayList<Pair<CensusMetadata, Polygon>>()
        List<CensusMetadata> censusMetadatas = readCensusMetadataFile(metadataFile, fipsCodeFields)
        List<Pair<Integer, Polygon>> idPolygons = readCensusLocationFile(locationFile)
        boolean useMap = false

        if(censusMetadatas.size() != idPolygons.size()){
            System.err.println("Warning: Metadata and location files do not have same number of entries.")
            useMap = true
        }
        if(!useMap){
            try{
                for(int i = 0; i < censusMetadatas.size(); i++){
                    CensusMetadata censusMetadata = censusMetadatas.get(i)
                    if(censusMetadata.isBlank){
                        continue
                    }
                    Pair<Integer, Polygon> idPolygon = idPolygons.get(i)
                    if(censusMetadata.id != idPolygon.first){
                        throw new Exception("Metadata and location files do not have matching ids across all entries.")
                    }
                    // Throw out invalid polygons.  Might be fixable with cleaner data.
                    if(idPolygon.second == null){
                        System.err.println("Throwing out invalid polygon: ${censusMetadata}")
                        continue
                    }
                    Polygon[] polygonArray = new Polygon[1]
                    polygonArray[0] = idPolygon.second
                    censusMetadataMultiPolygons.add(new Pair<CensusMetadata, MultiPolygon>(
                            first:censusMetadata, second:geometryFactory.createMultiPolygon(polygonArray))
                    )
                }
            }catch(Exception e){
                System.err.println("Warning: ${e.getMessage()}")
                censusMetadataMultiPolygons.clear()
                useMap = true
            }
        }
        if(useMap){
            Map<Integer, Polygon> idPolygonMap = new HashMap<Integer, Polygon>()
            for(idPolygon in idPolygons){
                idPolygonMap.put(idPolygon.first, idPolygon.second)
            }
            for(int i = 0; i < censusMetadatas.size(); i++){
                CensusMetadata censusMetadata = censusMetadatas.get(i)
                if(censusMetadata.isBlank){
                    idPolygonMap.remove(censusMetadata.id)
                    continue
                }
                Polygon polygon = idPolygonMap.get(censusMetadata.id)
                if(polygon != null){
                    Polygon[] polygonArray = new Polygon[1]
                    polygonArray[0] = polygon
                    censusMetadataMultiPolygons.add(new Pair<CensusMetadata, MultiPolygon>
                        (first:censusMetadata, second:geometryFactory.createMultiPolygon(polygonArray)))
                }
            }
        }
        if(mergePolygons){
            List<Pair<CensusMetadata, MultiPolygon>> oldCensusMetadataMultiPolygons = censusMetadataMultiPolygons
            censusMetadataMultiPolygons = new ArrayList<Pair<CensusMetadata, MultiPolygon>>()
            Map<String, List<MultiPolygon>> fipsCodeCensusMetadataMap = new HashMap<Integer, CensusMetadata>()
            Map<String, List<MultiPolygon>> fipsCodeMultiPolygonMap = new HashMap<Integer, List<MultiPolygon>>()
            for(censusMetadataMultiPolygon in oldCensusMetadataMultiPolygons){
                CensusMetadata censusMetadata = censusMetadataMultiPolygon.first
                MultiPolygon multiPolygon = censusMetadataMultiPolygon.second
                String fipsCode = ""
                for(int i = 0; i < mergeFipsCodeFields.size(); i++){
                    fipsCode += censusMetadata.fipsCodes.get(mergeFipsCodeFields.get(i))
                }
                if(!fipsCodeCensusMetadataMap.containsKey(fipsCode)){
                    fipsCodeCensusMetadataMap.put(fipsCode, censusMetadata)
                    fipsCodeMultiPolygonMap.put(fipsCode, new ArrayList<MultiPolygon>())
                }
                fipsCodeMultiPolygonMap.get(fipsCode).add(multiPolygon)
            }
            for(key in fipsCodeCensusMetadataMap.keySet()){
                CensusMetadata censusMetadata = fipsCodeCensusMetadataMap.get(key)
                List<MultiPolygon> multiPolygonList = fipsCodeMultiPolygonMap.get(key)
                Polygon[] polygonArray = new Polygon[multiPolygonList.size()]
                for(int i = 0; i < multiPolygonList.size(); i++){
                    polygonArray[i] = multiPolygonList.get(i).getGeometryN(0)
                }
                censusMetadataMultiPolygons.add(new Pair<CensusMetadata, MultiPolygon>
                    (first:censusMetadata, second:geometryFactory.createMultiPolygon(polygonArray)))
            }
        }
        return censusMetadataMultiPolygons
    }
    /**
     *  Read a metadata file.  Metadata files are simply lists of metadata
     *  chunks (processed by readCensusMetadataChunk()) separated by blank
     *  lines.  This method assembles those chunks into a list.
     *
     *  See the JavaDoc for readCensusData() for an explanation of
     *  fipsCodeFields.
     */
    private List<CensusMetadata> readCensusMetadataFile(String file, List<Integer> fipsCodeFields){
        BufferedReader reader = new BufferedReader(new FileReader(new File(file)))
        int numFields = 1
        assert Integer.parseInt(reader.readLine().trim()) == 0

        String line
        while((line = reader.readLine()).equals(" \" \"")){
            numFields++
        }

        List<CensusMetadata> censusMetadatas = new ArrayList<CensusMetadata>()
        CensusMetadata cm
        while((cm = readCensusMetadataChunk(reader, numFields, fipsCodeFields)) != null){
            censusMetadatas.add(cm)
        }
        return censusMetadatas
    }
    /**
     *  Read a metadata chunk.  numFields defines the number of fields.  See the
     *  JavaDoc for readCensusData() for an explanation of fipsCodeFields.
     */
    private CensusMetadata readCensusMetadataChunk(BufferedReader reader, int numFields, List<Integer> fipsCodeFields){
        /* This method assumes that census info chunks have six fields, of the
         * form:
         *
         * ID
         * State Code (or similar)
         * Congressional District/County/Etc. Code
         * Name (for congressional districts, etc, typically just a number)
         * Unknown Code (seems to designate what sort of entity--city, county...)
         * Entity Name (city, county...tied to the field above?)
         *
         * This may vary.  In particular, some data may omit one of fields 2 and 3.
         * There may be other changes, as well.  Adjust code below as necessary.
         */
        String line
        while(true){
            line = reader.readLine()
            if(line == null){
                return null
            }else if(!line.trim().equals("")){
                break
            }
        }

        int id = -1
        List<String> fipsCodes = new ArrayList<String>()
        String name
        String entityCode
        String entityName

        int numFieldsRead = 1

        try{
            id = Integer.parseInt(line.trim())
            while(numFieldsRead < numFields){
                line = reader.readLine().trim()
                line = line.substring(1, line.length() - 1)
                numFieldsRead++
                if(fipsCodeFields.contains(numFieldsRead - 1)){
                    if(line.trim().equals("")){
                        throw new RuntimeException("Chunk is incorrectly formatted.")
                    }
                    fipsCodes.add(line)
                }else if(name == null){
                    name = line
                }else if(entityCode == null){
                    entityCode = line
                }else if(entityName == null){
                    entityName = line
                }else{
                    throw new RuntimeException("Chunk is incorrectly formatted.")
                }
            }

            CensusMetadata cm = new CensusMetadata(
                id:id,
                fipsCodes:fipsCodes,
                name:name,
                entityCode:entityCode,
                entityName:entityName
            )

            return cm
        }catch(Exception e){
            for(int i = numFieldsRead; i < numFields; i++){
                line = reader.readLine()
            }
            return new CensusMetadata(id:id, fipsCodes:null, name:"", entityCode:"", entityName:"", isBlank:true)
        }

    }
    /**
     * Read a location file.  Location files are simply lists of location chunks
     * (processed by readCensusLocationChunk()) separated by blank lines.  This
     * method assembles those chunks into a list of IDs connected to polygons.
     * (The IDs determine which location chunk goes with which metadata chunk.
     * They are NOT FIPS codes.)
     */
    private List<Pair<Integer, Polygon>> readCensusLocationFile(String file){
        BufferedReader reader = new BufferedReader(new FileReader(new File(file)))
        List<Pair<Integer, Polygon>> polygons = new ArrayList<Pair<Integer, Polygon>>()
        Pair<Integer, List<Coordinate>> idCoordinatesPair
        int currentId
        int id
        List<Coordinate> currentCoordinates
        List<Coordinate> coordinates
        List<List<Coordinate>> holes = new ArrayList<List<Coordinate>>()

        while(true){
            idCoordinatesPair = readCensusLocationChunk(reader)
            if(idCoordinatesPair != null){
                currentId = idCoordinatesPair.first
                currentCoordinates = idCoordinatesPair.second
            }
            if(idCoordinatesPair == null || currentId != -99999){
                if(coordinates != null){
                    LinearRing lrPolygon = geometryFactory.createLinearRing(CoordinateArrays.toCoordinateArray(coordinates))
                    for(int i = holes.size() - 1; i >= 0; i--){
                        List<Coordinate> hole = holes.get(i)
                        if(hole.size() <= 3 && hole.size() != 0){
                            holes.remove(hole)
                        }
                    }
                    LinearRing[] lrHoles = new LinearRing[holes.size()]
                    for(int i = 0; i < holes.size(); i++){
                        try{
                            Coordinate[] lrHoleCoordinateArray = CoordinateArrays.toCoordinateArray(holes.get(i))
                            if(!lrHoleCoordinateArray[0].equals(lrHoleCoordinateArray[lrHoleCoordinateArray.length - 1])){
                                Coordinate[] newArray = Arrays.copyOf(lrHoleCoordinateArray, lrHoleCoordinateArray.length + 1)
                                newArray[newArray.length - 1] = newArray[0]
                                lrHoleCoordinateArray = newArray
                            }
                            lrHoles[i] = geometryFactory.createLinearRing(lrHoleCoordinateArray)
                        }catch(Exception e){
                            System.out.println "Error in readCensusLocationFile(), polygon currentId ${currentId}."
                            e.printStackTrace()
                        }
                    }
                    Polygon polygon = geometryFactory.createPolygon(lrPolygon, lrHoles)
                    if(!polygon.isValid()){
                        polygon = repairPolygon(polygon)    // *Attempt* to repair; null if failure
                    }
                    polygons.add(new Pair<Integer, Polygon>(first:id, second:polygon))
                }else{
                    assert holes.isEmpty()
                }

                if(idCoordinatesPair == null){
                    break
                }
                coordinates = currentCoordinates
                id = currentId
                holes.clear()
            }else{
                if(currentId == -99999){
                    assert(coordinates != null)
                    holes.add(currentCoordinates)
                }
            }
        }
        return polygons
    }
    /**
     *  Read a location chunk.  Returns a pair linking the ID of the chunk to
     *  the polygon defined by the chunk.
     */
    private Pair<Integer, List<Coordinate>> readCensusLocationChunk(BufferedReader reader){
        /* This reads data from ASCII files provided on the census website.
         * These files have chunks of the following format:
         *
         * id  long  lat
         * long  lat
         * long  lat
         * ...
         * END
         *
         * (The long/lat pair coming after the id is not a part of the boundary;
         * it is a point internal to the polygon.  It is ignored here.)
         *
         * After "END", there may be another chunk with id "-99999".  This
         * indicates that this chunk defines a hole in the previous chunk.
         */
        String line = reader.readLine()
        if(line == "END"){
            return null
        }

        int id
        String[] lineTokens
        float latitude
        float longitude
        List<Coordinate> coordinates = new ArrayList<Coordinate>()

        lineTokens = line.split(whitespaceRegex)
        id = Integer.parseInt(lineTokens[1])

        while(!(line = reader.readLine()).equals("END")){
            lineTokens = line.split(whitespaceRegex)
            latitude = Float.parseFloat(lineTokens[1])
            longitude = Float.parseFloat(lineTokens[2])
            coordinates.add(new Coordinate(latitude, longitude))
        }

        return new Pair<Integer, List<Coordinate>>(first:id, second:coordinates)
    }
    
    /* UTILITY */
    public void repairMissingMultiPolygon(Jurisdiction juris, String stateId){
        stateIdFipsCodeMap = mapStateIdsToFipsCodes(STATE_METAFILE)
        countyIdFipsCodeMap = mapCountyIdsToFipsCodes(COUNTY_METAFILE)
        State state = State.findByName(stateId)
        County county = ((VotingDistrict)juris).county
        String fipsCode = stateIdFipsCodeMap.get(state.id)
        String metafile = "${VD_PATH}/${VD_PREFIX}${fipsCode}${VD_METASUFFIX}"
        String locfile = "${VD_PATH}/${VD_PREFIX}${fipsCode}${VD_LOCSUFFIX}"
        List<Pair<CensusMetadata, MultiPolygon>> censusData = readCensusData(metafile, locfile, [1,2,3], true, [1,2])
        for(pair in censusData){
            String name = pair.first.name
            if(!name.equals(juris.name) || County.get(countyIdFipsCodeMap.getKey(pair.first.fipsCodes.get(0) + pair.first.fipsCodes.get(1))) != county){
                continue
            }
            MultiPolygon polygon = pair.second
            juris.multiPolygon = polygon
            jurisdictionService.save(juris, true)
            println "${juris.name} repaired."
            GeneralUtils.wipeReferences(sessionFactory)
            return
        }
    }
    /**
     *  Use the state-name-to-postal-code file to generate a bidirectional map
     *  linking state names to postal codes.  The state names are generated from
     *  the file, NOT from the database, but match the database data.  They are
     *  in uppercase, however.
     */
    private BidiMap mapStateNamesToPostalCodes(){
        BidiMap stateNamePostalCodeMap = new DualHashBidiMap()
        BufferedReader reader = new BufferedReader(new FileReader(new File(STATE_POSTALCODEFILE)))
        reader.readLine()
        reader.readLine()
        String line
        String[] lineTokens
        while(!(line = reader.readLine()).trim().equals("")){
            lineTokens = line.split(whitespaceRegex)
            String stateName = ""
            for(int i = 0; i < lineTokens.length - 1; i++){
                stateName += lineTokens[i]
                if(i < lineTokens.length - 2){
                    stateName += " "
                }
            }
            stateNamePostalCodeMap.put(stateName, lineTokens[lineTokens.length - 1])
        }
        return stateNamePostalCodeMap
    }
    /**
     *  Create a bidirection map linking state IDs (that is, the IDs of the
     *  State objects in our database) to their FIPS codes, as declared by the
     *  stateMetadataFile.
     */
    private BidiMap mapStateIdsToFipsCodes(String stateMetadataFile){
        if(stateIdFipsCodeMap == null){
            stateIdFipsCodeMap = new DualHashBidiMap()
            List<CensusMetadata> censusMetadatas = readCensusMetadataFile(stateMetadataFile, [1])
            for(censusMetadata in censusMetadatas){
                String fipsCode = censusMetadata.fipsCodes.get(0)
                String stateName = censusMetadata.name
                State state = State.findByLongName(stateName)
                if(state != null){
                    stateIdFipsCodeMap.put(state.id, fipsCode)
                }
            }
        }
        return stateIdFipsCodeMap
    }
    private BidiMap mapCountyIdsToFipsCodes(String countyMetadataFile){
        mapStateIdsToFipsCodes(STATE_METAFILE)
        if(countyIdFipsCodeMap == null){
            countyIdFipsCodeMap = new DualHashBidiMap()
            List<CensusMetadata> censusMetadatas = readCensusMetadataFile(countyMetadataFile, [1,2])
            for(censusMetadata in censusMetadatas){
                String fipsCode = censusMetadata.fipsCodes.get(0) + censusMetadata.fipsCodes.get(1)
                String countyName = censusMetadata.name + " " + censusMetadata.entityName
                State state = State.get(stateIdFipsCodeMap.getKey(censusMetadata.fipsCodes.get(0)))
                County county = County.findByLongNameAndState(countyName, state)
                if(county != null){
                    countyIdFipsCodeMap.put(county.id, fipsCode)
                }
            }
        }
        return countyIdFipsCodeMap
    }
    /*
     * Detect and repair errors in polygons.  Not very robust, but gets the job
     * done.
     */
    private Polygon repairPolygon(Polygon polygon){
        IsValidOp ivo = new IsValidOp(polygon)
        TopologyValidationError error = ivo.getValidationError()
        // "Self-intersection at or near point (##, ##, NaN)"
        if(error.getErrorType() == TopologyValidationError.SELF_INTERSECTION){
            while((polygon != null) && !polygon.isValid()){
                polygon = repairPolygonSelfIntersection(polygon, error)
            }
        }
        return polygon
    }
    /*
     * Repair self-intersection problems by throwing out the nearest point.  Not
     * very smart; you'll probably have to do this repeatedly until the problem
     * goes away.
     */
    private Polygon repairPolygonSelfIntersection(Polygon polygon, TopologyValidationError error){
        int totalCoordinates = 0
        for(int i = 0; i < polygon.getNumInteriorRing(); i++){
            totalCoordinates += polygon.getInteriorRingN(i).getCoordinates().length
        }
        totalCoordinates += polygon.getExteriorRing().getCoordinates().length
        Coordinate errorCoordinate = error.getCoordinate()
        Coordinate[][] coordinates = new Coordinate[polygon.getNumInteriorRing() + 1][]
        coordinates[0] = polygon.getExteriorRing().getCoordinates()
        for(int i = 1; i < coordinates.length; i++){
            coordinates[i] = polygon.getInteriorRingN(i - 1).getCoordinates()
        }
        int minCoordinateIndex = -1
        int minCoordinateSubIndex = -1
        double minDistance = 1000
        for(int i = 0; i < coordinates.length; i++){
            if((i == 0) && (coordinates[0].length <= 4)){
                continue
            }
            for(int j = 0; j < coordinates[i].length; j++){
                double currentDistance = errorCoordinate.distance(coordinates[i][j])
                if(currentDistance < minDistance){
                    minCoordinateIndex = i
                    minCoordinateSubIndex = j
                    minDistance = currentDistance
                }
            }
        }
        if(minCoordinateIndex == -1){
            return null
        }

        if(coordinates[minCoordinateIndex].length <= 4){
            Coordinate[][] newCoordinates = new Coordinate[coordinates.length - 1][]
            for(int i = 0; i < minCoordinateIndex; i++){
                newCoordinates[i] = coordinates[i]
            }
            for(int i = minCoordinateIndex + 1; i < coordinates.length; i++){
                newCoordinates[i - 1] = coordinates[i]
            }
            coordinates = newCoordinates
        }else{
            Coordinate[] newCoordinates = new Coordinate[coordinates[minCoordinateIndex].length - 1]
            for(int i = 0; i < minCoordinateSubIndex; i++){
                newCoordinates[i] = coordinates[minCoordinateIndex][i]
            }
            for(int i = minCoordinateSubIndex + 1; i < coordinates[minCoordinateIndex].length; i++){
                newCoordinates[i - 1] = coordinates[minCoordinateIndex][i]
            }

            // In case removed coordinate was first one
            newCoordinates[newCoordinates.length - 1] = newCoordinates[0]

            coordinates[minCoordinateIndex] = newCoordinates
        }

        LinearRing lrPolygon = new LinearRing(
            new CoordinateArraySequence(coordinates[0]),
            geometryFactory
        )
        LinearRing[] lrHoles = new LinearRing[coordinates.length - 1]
        for(int i = 1; i < coordinates.length; i++){
            lrHoles[i - 1] = new LinearRing(
                new CoordinateArraySequence(coordinates[i]),
                geometryFactory
            )
        }

        Polygon newPolygon = new Polygon(
            lrPolygon,
            lrHoles,
            geometryFactory
        )
        return newPolygon
    }
    private boolean listContainsString(List<String> strList, String str){
        for(element in strList){
            if(element.equals(str)){
                return true
            }
        }
        return false
    }
}
